package com.pocket.demo.base.exception.handler;

import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.json.JSONUtil;
import com.pocket.demo.base.api.R;
import com.pocket.demo.base.consts.SystemConfigConst;
import com.pocket.demo.base.enums.MsgCodeEnum;
import com.pocket.demo.base.exception.BusinessException;
import com.pocket.demo.base.exception.PocketValidateException;
import com.pocket.demo.base.utils.ServletUtils;
import com.pocket.demo.core.i18n.MessageSourceService;
import com.pocket.demo.core.validation.ValidationEntity;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.hibernate.validator.internal.engine.ConstraintViolationImpl;
import org.hibernate.validator.internal.engine.path.PathImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.WebUtils;

import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * <p> 全局异常统一处理拦截器 <br>
 * <p> 具体处理类型异常 <br>
 * <p> 1. 参数类型异常 <br>
 *      1.1. MethodArgumentTypeMismatchException <br>
 *      1.2. BindException <br>
 *      1.3. MethodArgumentNotValidException <br>
 *      1.4. ConstraintViolationException <br>
 *     参考：<a href="https://blog.csdn.net/qq_26222859/article/details/54272747"> <br>
 * <p> 2. 自定义业务异常 <br>
 *      2.1. BusinessException <br>
 *
 * <p> 扩展说明 <br>
 * <p> @RestControllerAdvice  <br>
 *     拦截 @RestController 注解修饰的类及其方法抛出的异常 <br>
 * <p> @ExceptionHandler 注解方法 入参、出参 说明： <br>
 *     参考：<a href="https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-exceptionhandler.html">官方参考</a> <br>
 * <p> GlobalExceptionHandler案例： <br>
 *     参考：<a href="https://blog.csdn.net/sgx1825192/category_12547858.html">GlobalExceptionHandler案例</a> <br>
 **/
@RestControllerAdvice(basePackages = {"com.pocket.demo"})
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @Resource
    private MessageSourceService messageSourceService;

    /**
     * <p> 参数处理异常(SpringMVC)：处理方法调用参数类型和方法定义的参数类型不匹配异常 <br>
     */
    @ExceptionHandler({MethodArgumentTypeMismatchException.class})
    public R<Object> handlerException(HttpServletResponse response, MethodArgumentTypeMismatchException ex) {
        // 【demo-i18n-2.4】
        R<Object> result = this.messageSourceService.getRestResult(MsgCodeEnum.ERROR_BAD_PARAMETER, new Object[] { "" });
        result.setDebug(getFullExceptionMessage(ex));

        response.setStatus(HttpStatus.BAD_REQUEST.value());
        logger.error(ex.getMessage(), ex);
        return result;
    }

    /**
     * 【demo-spring-validation-3】
     * <p> 参数处理异常(validation)<br>
     *     表单(Content-Type: multipart/form-data)方式提交 && 绑定参数（方法入参）为Java bean && 参数使用 @Valid 注解 <br>
     *     TODO 暂未复现此异常
     * @param ex
     * @return
     */
    @ExceptionHandler({BindException.class})
    public R<Object> handlerException(BindException ex) {
        return R.fail(MsgCodeEnum.ERROR_VALID_BAD_PARAMETER.getCode(), ex.getMessage(), MsgCodeEnum.ERROR_VALID_BAD_PARAMETER.getMsgKey());
    }


    /**
     * 【demo-spring-validation-3】
     * <p> 参数处理异常(validation)：处理标注 @Validated @Valid 参数校验失败异常的字段 <br>
     *     表单(Content-Type: application/json、Content-Type: application/xml)方式提交 && 绑定参数（方法入参）为javabean类型 && 参数使用 @RequestBody @Valid 注解 <br>
     *
     * @param ex
     * @return
     */
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public R<Object> handlerException(MethodArgumentNotValidException ex) {
        String fieldMessagePrefix = AnnotationUtil.getAnnotationValue(ex.getTarget().getClass(), ValidationEntity.class);

        // 解析实体类上的 @ValidationEntity 值 + 字段名 对其国际化
        String fieldEntity = StringUtils.isBlank(fieldMessagePrefix) ? StringUtils.EMPTY : fieldMessagePrefix.concat(".");
        String errorMessage = ex.getBindingResult().getAllErrors().stream().map(
                item -> {
                    if (item instanceof FieldError){
                        return this.messageSourceService.getMessage(fieldEntity + ((FieldError) item).getField()) + item.getDefaultMessage();
                    }
                    return item.getDefaultMessage();
                }
        ).collect(Collectors.joining(System.lineSeparator()));

        return R.fail(MsgCodeEnum.ERROR_VALID_BAD_PARAMETER.getCode(), errorMessage, MsgCodeEnum.ERROR_VALID_BAD_PARAMETER.getMsgKey());
    }

    /**
     * 【demo-spring-validation-3】
     * <p> 参数处理异常(validation) <br>
     *     校验方法类上使用 @Validated 注解 && 方法入参为简单参数类型（String、Integer） && 参数前使用校验规则注解（@Pattern） <br>
     * @param ex
     * @return
     */
    @ExceptionHandler({ConstraintViolationException.class})
    public R<Object> handlerException(ConstraintViolationException ex) {
        return R.fail(MsgCodeEnum.ERROR_VALID_BAD_PARAMETER.getCode(), ex.getMessage(), MsgCodeEnum.ERROR_VALID_BAD_PARAMETER.getMsgKey());
    }

    /**
     * <p> 处理 Spring Validator 手动校验异常国际化【demo-spring-validation-4】
     * @param ex
     * @return
     */
    @ExceptionHandler({PocketValidateException.class})
    public R<Object> handlerException(PocketValidateException ex) {
        Set<ConstraintViolation> violations = ex.getResult();

        String errorMessage = violations.stream().map(
                item -> {
                    if (item instanceof ConstraintViolationImpl<?>){
                        String fieldEntity = AnnotationUtil.getAnnotationValue(item.getRootBeanClass(), ValidationEntity.class);
                        ConstraintViolationImpl itemImpl = (ConstraintViolationImpl) item;
                        String fieldName = ((PathImpl) itemImpl.getPropertyPath()).getLeafNode().getName();
                        String messageKeyPrefix = StringUtils.isBlank(fieldEntity) ? StringUtils.EMPTY : fieldEntity.concat(".");
                        return this.messageSourceService.getMessage(messageKeyPrefix + fieldName) + item.getMessage();
                    }
                    return item.getMessage();
                }
        ).collect(Collectors.joining(System.lineSeparator()));

        return R.fail(MsgCodeEnum.ERROR_VALID_BAD_PARAMETER.getCode(), errorMessage, MsgCodeEnum.ERROR_VALID_BAD_PARAMETER.getMsgKey());
    }

    /**
     * 处理访问资源无权限问题
     * @param ex
     * @return
     */
    @ExceptionHandler({AccessDeniedException.class})
    public R<Object> handAccessDeniedException(BusinessException ex){
        logger.warn("AccessDeniedException: {}", ex.getMessage(), ex);
        return MessageSourceService.getRestResult(MsgCodeEnum.ERROR_UNAUTHORIZED, new Object[0]);
    }

    /**
     * <p> 业务异常：BusinessException <br>
     * @param response
     * @param ex
     * @return
     */
    @ExceptionHandler({BusinessException.class})
    public R<Object> handlerException(HttpServletResponse response, BusinessException ex) {
        R<Object> result = this.messageSourceService.getResultFromBusinessException(ex);
        result.setDebug(getFullExceptionMessage(ex));

        response.setStatus(HttpStatus.BAD_REQUEST.value());
        if (!result.getCode().equals(MsgCodeEnum.SUCCESS.getCode())) {
            response.setStatus(MsgCodeEnum.codeToHttpStatus(result.getCode()).intValue());
        }

        logger.error(ex.getMessage(), ex);
        return result;
    }

    /**
     * <p> 兜底异常：BusinessException <br>
     * @param response
     * @param ex
     * @return
     */
    @ExceptionHandler({Exception.class})
    public R<Object> handlerException(HttpServletResponse response, Exception ex) {
        response.setStatus(HttpStatus.BAD_REQUEST.value());
        // TODO 发送异常到指定系统 如：sentry
        R<Object> result = this.messageSourceService.getRestResult(MsgCodeEnum.ERROR, new Object[] { "" });
        result.setDebug(getFullExceptionMessage(ex));
        logger.error(ex.getMessage(), ex);
        return result;
    }

    private String getFullExceptionMessage(Throwable e) {
        if (e == null) {
            return null;
        }
        return String.join("\n", Arrays.asList( ExceptionUtils.getRootCauseMessage(e), ExceptionUtils.getStackTrace(e) ) );
    }

    private String getPayload(HttpServletRequest request) {
        StringBuilder body = new StringBuilder();
        ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
        if (wrapper != null) {
            byte[] buf = wrapper.getContentAsByteArray();
            if (buf.length > 0) {
                String payload; int length = Math.min(buf.length, SystemConfigConst.MAX_PAYLOAD_LENGTH);
                try {
                    payload = new String(buf, 0, length, wrapper.getCharacterEncoding());
                } catch (UnsupportedEncodingException ex) {
                    payload = "[unknown]";
                }
                body.append(payload);
            }
        }
        return body.toString();
    }

    private Map<String, Object> getSentryExtra(HttpServletRequest request){
        Map<String, Object> sentryExtra = new HashMap<>();
        sentryExtra.put("request_method", request.getMethod());
        sentryExtra.put("query_string", request.getQueryString());
        sentryExtra.put("request_uri", request.getRequestURI());
        sentryExtra.put("payload", getPayload(request));
        String host = request.getHeader("Host");
        sentryExtra.put("host", host);
        String referer = request.getHeader("Referer");
        sentryExtra.put("referer", referer);
        return sentryExtra;
    }
}
